/*
 * Copyright 2011 Michele Mancioppi [michele.mancioppi@gmail.com]
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cave.nice.testMessage.data;

import java.io.Closeable;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.mail.Header;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.NoResultException;
import javax.persistence.Persistence;

import org.apache.commons.io.IOUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;

import cave.nice.testMessage.data.ReportingPolicy.ReportingPolicyType;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Text;

public class DataManager implements Closeable {

	private final class AccountByRegistrationDateComparator implements
			Comparator<Account> {
		@Override
		public int compare(Account o1, Account o2) {
			return o1.getRegistrationDate().compareTo(o2.getRegistrationDate());
		}
	}

	private static final Logger LOGGER = Logger.getLogger(DataManager.class
			.getName());

	private static final DateTimeZone TIME_ZONE = DateTimeZone
			.forID("Europe/Berlin");

	private static final EntityManagerFactory ENTITY_MANAGER_FACTORY = Persistence
			.createEntityManagerFactory("transactions-optional");

	public static DataManager getInstance() {
		return new DataManager();
	}

	private final EntityManager entityManager;

	boolean closed = false;

	private DataManager() {
		entityManager = ENTITY_MANAGER_FACTORY.createEntityManager();
	}

	@Override
	public void close() {
		this.entityManager.close();
		closed = true;
	}

	@Override
	protected void finalize() throws Throwable {
		try {
			if (!closed) {
				close();
			}
		} finally {
			super.finalize();
		}
	}

	public Test createTest(InternetAddress accountEMailAddress, Date testDate,
			UUID challenge) throws UnknownVerifiedAccountException {
		return createTest(getVerifiedAccount(accountEMailAddress), testDate,
				challenge);
	}

	public Test createTest(VerifiedAccount account, Date testDate,
			UUID challenge) {
		Test newTest = new Test();
		newTest.setAccount(account);
		newTest.setDate(testDate);
		newTest.setChallenge(challenge.toString());

		EntityTransaction transaction = entityManager.getTransaction();
		try {
			transaction.begin();
			entityManager.persist(newTest);
			transaction.commit();
			return newTest;
		} finally {
			if (transaction.isActive()) {
				transaction.rollback();
			}
		}
	}

	public UnverifiedAccount createUnverifiedAccount(
			InternetAddress emailAddress) {
		UnverifiedAccount newAccount = new UnverifiedAccount();
		newAccount.setEmailAddress(emailAddress.getAddress());
		newAccount.setRegistrationDate(new Date());
		newAccount.setChallenge(UUID.randomUUID().toString());

		EntityTransaction transaction = entityManager.getTransaction();
		try {
			transaction.begin();
			entityManager.persist(newAccount);
			transaction.commit();
			return newAccount;
		} finally {
			if (transaction.isActive()) {
				transaction.rollback();
			}
		}
	}

	public VerifiedAccount createVerifiedAccount(InternetAddress emailAddress,
			List<ReportingPolicy> reportingPolicies) {
		VerifiedAccount newAccount = new VerifiedAccount();
		newAccount.setEmailAddress(emailAddress.getAddress());
		newAccount.setRegistrationDate(new Date());
		newAccount.setReportingPolicies(reportingPolicies);

		EntityTransaction transaction = entityManager.getTransaction();
		try {
			transaction.begin();
			entityManager.persist(newAccount);
			transaction.commit();
			return newAccount;
		} finally {
			if (transaction.isActive()) {
				transaction.rollback();
			}
		}
	}

	public <T extends Account> T getAccount(InternetAddress emailAddress,
			Class<T> clazz) throws UnknownEmailAddressException {
		try {
			return clazz.cast(entityManager.createQuery(
					"select from " + clazz.getName() + " where emailAddress='"
							+ emailAddress.toString() + "'").getSingleResult());
		} catch (Exception e) {
			throw new UnknownEmailAddressException(e);
		}
	}

	public List<Test> getClosedTests(VerifiedAccount account)
			throws CannotRetrieveEntitiesException {
		return getTests(account, true);
	}

	public DateTime getCurrentTime() {
		return new DateTime(TIME_ZONE);
	}

	public List<Test> getOpenTests(VerifiedAccount account)
			throws CannotRetrieveEntitiesException {
		return getTests(account, false);
	}

	public Test getTest(Key testIdentifier)
			throws UnknownTestIdentifierException {
		Test test = entityManager.find(Test.class, testIdentifier);

		if (test == null) {
			throw new UnknownTestIdentifierException();
		}

		return test;
	}

	public Test getTestByChallenge(UUID challenge)
			throws UknownTestChallengeException {
		try {
			return (Test) entityManager.createQuery(
					"select from " + Test.class.getName()
							+ " where challenge = '" + challenge + "'")
					.getSingleResult();
		} catch (Exception e) {
			throw new UknownTestChallengeException(e);
		}
	}

	public Test getTestOfAccount(VerifiedAccount account, UUID testChallenge)
			throws UnknownVerifiedAccountException,
			UknownTestChallengeException, WrongAccountForTestException {
		try {
			Test test = getTestByChallenge(testChallenge);

			if (!test.getAccount().equals(account)) {
				throw new WrongAccountForTestException(
						"The test with challenge '"
								+ testChallenge
								+ "' (id: "
								+ test.getIdentifier()
								+ ") is not associated with the account with email address '"
								+ account.getEmailAddress() + "'");
			}

			return test;
		} catch (NoResultException e) {
			throw new UknownTestChallengeException(e);
		}
	}

	public List<Test> getTests(VerifiedAccount account, boolean answered)
			throws CannotRetrieveEntitiesException {
		try {
			@SuppressWarnings("unchecked")
			List<Test> openTests = entityManager
					.createQuery(
							"select from " + Test.class.getName()
									+ " where account=?1 and answeredOn is "
									+ ((answered) ? "not" : "") + " null")
					.setParameter(1, account).getResultList();
			Collections.sort(openTests, new Test.TestComparatorByDate());

			return openTests;
		} catch (Exception e) {
			throw new CannotRetrieveEntitiesException(e);
		}
	}

	public UnverifiedAccount getUnverifiedAccount(InternetAddress address)
			throws UnknownUnverifiedAccountException {
		try {
			return getAccount(address, UnverifiedAccount.class);
		} catch (UnknownEmailAddressException e) {
			throw new UnknownUnverifiedAccountException(e);
		}
	}

	public UnverifiedAccount getUnverifiedAccount(Key accountIdentifier)
			throws UnknownVerifiedAccountException {
		try {
			return entityManager.find(UnverifiedAccount.class,
					accountIdentifier);
		} catch (IllegalArgumentException e) {
			throw new UnknownVerifiedAccountException(e);
		}
	}

	public List<UnverifiedAccount> getUnverifiedAccounts()
			throws CannotRetrieveEntitiesException {
		return getAccounts(UnverifiedAccount.class);
	}

	public VerifiedAccount getVerifiedAccount(InternetAddress address)
			throws UnknownVerifiedAccountException {
		try {
			return getAccount(address, VerifiedAccount.class);
		} catch (UnknownEmailAddressException e) {
			throw new UnknownVerifiedAccountException(e);
		}
	}

	public VerifiedAccount getVerifiedAccount(Key accountIdentifier)
			throws UnknownVerifiedAccountException {
		try {
			return entityManager.find(VerifiedAccount.class, accountIdentifier);
		} catch (IllegalArgumentException e) {
			throw new UnknownVerifiedAccountException(e);
		}
	}

	public List<VerifiedAccount> getVerifiedAccounts()
			throws CannotRetrieveEntitiesException {
		return getAccounts(VerifiedAccount.class);
	}

	private <T extends Account> List<T> getAccounts(Class<T> clazz)
			throws CannotRetrieveEntitiesException {
		try {
			@SuppressWarnings("unchecked")
			List<T> accounts = entityManager.createQuery(
					"select from " + clazz.getName()).getResultList();
			Collections.sort(accounts,
					new AccountByRegistrationDateComparator());

			return accounts;
		} catch (Exception e) {
			throw new CannotRetrieveEntitiesException(e);
		}
	}

	public void markTestAsAnswered(Test test, Date answerTime)
			throws CannotUpdateEntityException, TestAlreadyAnsweredException {
		if (test.isAnswered()) {
			throw new TestAlreadyAnsweredException();
		}

		EntityTransaction transaction = entityManager.getTransaction();
		try {
			transaction.begin();
			test.setAnsweredOn(answerTime);
			transaction.commit();
		} catch (Exception e) {
			throw new CannotUpdateEntityException(e);
		} finally {
			if (transaction.isActive()) {
				transaction.rollback();
			}
		}
	}

	public void removeAccount(Account account) {
		EntityTransaction transaction = entityManager.getTransaction();
		try {
			/*
			 * FIXME! Re-enable the line below and remove the second transaction
			 * as soon as Google rolls-out the Cross-group transactions
			 */
			// entityManager.remove(unverifiedAccount);
			transaction.begin();
			/*
			entityManager.createQuery(
					"delete from " + account.getClass().getName()
							+ " where emailAddress='"
							+ account.getEmailAddress() + "'").executeUpdate();
			*/
			entityManager.remove(account);
			//
			transaction.commit();
		} finally {
			if (transaction.isActive()) {
				transaction.rollback();
			}
		}
	}

	public void removeExpiredUnverifiedAccounts() {
		DateTime currentDate = getCurrentTime();
		Date threshold = currentDate.minus(UnverifiedAccount.EXPIRATION_PERIOD)
				.toDate();

		int removedAccounts = entityManager
				.createQuery(
						"delete " + UnverifiedAccount.class.getName()
								+ " where registrationDate < $1")
				.setParameter(1, threshold).executeUpdate();
		LOGGER.info("Deleted " + removedAccounts
				+ " unverified accounts past validity date");
	}

	public void removeTest(Test... tests) {
		EntityTransaction transaction = entityManager.getTransaction();
		try {
			transaction.begin();
			for (Test test : tests) {
				entityManager.remove(test);
			}
			transaction.commit();
		} finally {
			if (transaction.isActive()) {
				transaction.rollback();
			}
		}
	}

	public UnprocessedEMail storeUnprocessedEMail(MimeMessage message)
			throws IOException {
		UnprocessedEMail unprocessedEMail = new UnprocessedEMail();

		try {
			StringWriter sw = new StringWriter();
			@SuppressWarnings("rawtypes")
			Enumeration headers = message.getAllHeaders();
			while (headers.hasMoreElements()) {
				Header header = (Header) headers.nextElement();
				sw.append(header.getName());
				sw.append('=');
				sw.append(header.getValue());
				sw.append('\r');
			}

			unprocessedEMail.setHeaders(new Text(sw.toString()));
			unprocessedEMail.setSubject(new Text(message.getSubject()));
			unprocessedEMail.setContent(new Text(IOUtils.toString(message
					.getInputStream())));

			EntityTransaction transaction = entityManager.getTransaction();
			try {
				transaction.begin();
				entityManager.persist(unprocessedEMail);
				transaction.commit();
				return unprocessedEMail;
			} finally {
				if (transaction.isActive()) {
					transaction.rollback();
				}
			}
		} catch (Exception e) {
			LOGGER.log(Level.SEVERE, "Cannot store unprocessed message: "
					+ message, e);
			throw new IOException("Cannot store unprocessed message: "
					+ message, e);
		}
	}

	public VerifiedAccount verifyAccount(InternetAddress emailAddress,
			UUID challenge) throws VerificationChallengeNotMetException {
		UnverifiedAccount unverifiedAccount = null;
		try {
			unverifiedAccount = (UnverifiedAccount) entityManager.createQuery(
					"select from " + UnverifiedAccount.class.getName()
							+ " where emailAddress='"
							+ emailAddress.getAddress() + "' and challenge='"
							+ challenge + "'").getSingleResult();
		} catch (NoResultException e) {
			throw new VerificationChallengeNotMetException();
		}

		VerifiedAccount newAccount = new VerifiedAccount();
		newAccount.setEmailAddress(unverifiedAccount.getEmailAddress());
		newAccount.setRegistrationDate(unverifiedAccount.getRegistrationDate());

		EntityTransaction transaction = entityManager.getTransaction();
		try {
			transaction.begin();
			/*
			 * FIXME! Re-enable the line below and remove the second transaction
			 * as soon as Google rolls-out the Cross-group transactions
			 */
			// entityManager.remove(unverifiedAccount);
			entityManager.persist(newAccount);
			transaction.commit();

			transaction.begin();
			entityManager.createQuery(
					"delete from " + UnverifiedAccount.class.getName()
							+ " where emailAddress='"
							+ unverifiedAccount.getEmailAddress() + "'")
					.executeUpdate();
			transaction.commit();

			return newAccount;
		} finally {
			if (transaction.isActive()) {
				transaction.rollback();
			}
		}
	}

	public ReportingPolicy addNewReportingPolicy(VerifiedAccount account,
			ReportingPolicyType type, Integer notifyAfterMinutes,
			Boolean notifyIfNoOpenTests)
			throws DuplicatedReportingPolicyException {
		for (ReportingPolicy reportingPolicy : account.getReportingPolicies()) {
			if (reportingPolicy.getType().equals(type)) {
				/*
				 * TODO Error message
				 */
				throw new DuplicatedReportingPolicyException();
			}
		}

		ReportingPolicy reportingPolicy = new ReportingPolicy();
		reportingPolicy.setType(type);
		reportingPolicy.setNotifyAfterMinutes(notifyAfterMinutes);
		reportingPolicy.setNotifyIfNoOpenTests(notifyIfNoOpenTests);
		reportingPolicy.setAccount(account);

		EntityTransaction transaction = entityManager.getTransaction();
		try {
			transaction.begin();
			entityManager.persist(reportingPolicy);
			account.getReportingPolicies().add(reportingPolicy);
			transaction.commit();
		} finally {
			if (transaction.isActive()) {
				transaction.rollback();
			}
		}

		return reportingPolicy;
	}

	public void removeReportingPolicy(VerifiedAccount account,
			ReportingPolicy reportingPolicy) {
		EntityTransaction transaction = entityManager.getTransaction();
		try {
			transaction.begin();
			account.getReportingPolicies().remove(reportingPolicy);
			entityManager.remove(reportingPolicy);
			transaction.commit();
		} finally {
			if (transaction.isActive()) {
				transaction.rollback();
			}
		}
	}

}
